package classification.classifier;

import static preprocess.XMLContracter.ATTRIBUTE;
import static preprocess.XMLContracter.NESTING;
import static preprocess.XMLContracter.REPETITION;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.StringTokenizer;

import org.dom4j.Document;
import org.dom4j.io.SAXReader;

import preprocess.XMLContracter;
import preprocess.XMLDoc;

import utils.Configuration;
import classification.calcSimilarity.CalcSimilarity;
import static utils.Configuration.*;

public abstract class Classifier
{
    protected final String            ROOT         = RootPath;
    protected final String            XMLs_PATH    = Configuration.CLASSIFICATION.classification_src_path;
    protected final int               CV_FACTOR    = 3;

    /* classify related object */
    protected double[]                precisionArr = null;
    protected HashMap<String, XMLDoc> XMLDocMap    = null;
    protected HashMap<String, String> categoryMap  = null;
    protected String[]                docIDSet     = null;
    protected File[]                  XMLFileList  = null;
    protected String                  mainDirPath  = null;
    protected SAXReader               reader       = null;
    protected XMLContracter           contracter   = null;

    /* classify parameters */
    protected String                  isBalance    = null;
    protected int                     category_num = 2;
    protected int                     KNN          = 1;

    /* IO */
    protected static BufferedReader   stdIn        = new BufferedReader(
                                                           new InputStreamReader(
                                                                   System.in));
    protected static PrintWriter      stdOut       = new PrintWriter(
                                                           System.out, true);
    protected static PrintWriter      stdErr       = new PrintWriter(
                                                           System.err, true);

    protected abstract void run();

    public Classifier()
    {
        this.reader = new SAXReader();
        this.contracter = new XMLContracter();
    }

    protected boolean init()
    {
        try
        {
            /* 得到程序运行所需参数 */
            stdOut
                    .println("Please input category numbers you want to classified: (2/4/8/16/21)");
            this.category_num = Integer.parseInt(stdIn.readLine());

            stdOut
                    .println("Do you want training xml files with same size? Y/N");
            if (stdIn.readLine().toLowerCase().equals("y"))
            {
                this.isBalance = "same";
            }
            else
            {
                this.isBalance = "different";
            }

            stdOut.println("Please input KNN numbers: (99/199/399)");
            this.KNN = Integer.parseInt(stdIn.readLine());

            /* 得到所有xml文件 */
            this.mainDirPath = this.XMLs_PATH + this.isBalance + "_size_"
                    + this.category_num;
            File mainDir = new File(this.mainDirPath);
            this.XMLFileList = this.getFileList(mainDir);

            /*
             * 找到所有XML文件的类别信息
             */
            if ((this.categoryMap = this.exploreCategory()) == null)
            {
                stdErr.println("Error occur in category explore process");
            }

            /* 创建记录cross-validation准确率的数组 */
            this.precisionArr = new double[this.CV_FACTOR];

            /* 创建XMLDoc的备忘录 */
            this.XMLDocMap = new HashMap<String, XMLDoc>();

            return true;
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
            return false;
        }
    }

    protected double crossValidation()
    {
        File[] fileList = this.XMLFileList;
        String XMLFileDir = this.mainDirPath;
        try
        {
            /* get files of each category */
            File mainDir = new File(XMLFileDir);
            ArrayList<File[]> categories = new ArrayList<File[]>();
            for (File ctgDir : mainDir.listFiles())
            {
                File[] ctgFiles = ctgDir.listFiles();
                categories.add(ctgFiles);
            }

            /*
             * get training and test xml file set and invoke cross validation
             * subprocess
             */
            ArrayList<File> trainingFilesArr = null;
            ArrayList<File> testFilesArr = null;
            int crossValidationIndex = 0;
            while (crossValidationIndex < this.CV_FACTOR)
            {
                stdErr
                        .println("******************the "
                                + (crossValidationIndex + 1)
                                + " time Cross-Validation is running.******************");
                stdOut.println();

                trainingFilesArr = new ArrayList<File>();
                testFilesArr = new ArrayList<File>();

                /* get training and test xml file set */
                for (File[] ctgFiles : categories)
                {
                    int ctgTotalSize = ctgFiles.length;
                    int traSize = ctgTotalSize / this.CV_FACTOR;

                    int traStartIndex = traSize * crossValidationIndex;

                    /* get the training file of this category */
                    for (int index = traStartIndex; index < traStartIndex
                            + traSize; index++)
                    {
                        trainingFilesArr.add(ctgFiles[index]);
                    }

                    /* get the test file of this category */
                    for (int index = 0; index < traStartIndex; index++)
                    {
                        testFilesArr.add(ctgFiles[index]);
                    }
                    for (int index = traStartIndex + traSize; index < ctgTotalSize; index++)
                    {
                        testFilesArr.add(ctgFiles[index]);
                    }
                }

                /* invoke cross validation training subprocess */
                if (!this.training(trainingFilesArr))
                {
                    stdErr.println("Error occur in training process");
                }

                /* invoke cross validation test subprocess */
                if ((this.precisionArr[crossValidationIndex] = this.test(
                        testFilesArr, trainingFilesArr)) == -1)
                {
                    stdErr.println("Error occur in test process");
                }

                stdErr.println("***the " + (crossValidationIndex + 1)
                        + " time Cross-Validation is end.***");
                stdOut.println();

                /* cross Validation Index increase */
                crossValidationIndex++;
            }

            /* 计算cross-Validation下的平均准确率 */
            double sum = 0.0;
            for (double prec : this.precisionArr)
            {
                sum += prec;
            }
            double avgPrec = sum / (double) this.precisionArr.length;

            return avgPrec;
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
            return -1;
        }
    }

    protected abstract boolean training(final ArrayList<File> fileArr);

    protected abstract double test(final ArrayList<File> fileArr,
            final ArrayList<File> traFileArr);

    protected ClassifyResult KNNValueClassify(final double[] resultItem,
            final String[] TraDocIDSet)
    {
        try
        {
            int[] assistArr = new int[resultItem.length];
            for (int i = 0; i < assistArr.length; i++)
            {
                assistArr[i] = i;
            }

            this.sort(resultItem, assistArr, 0, resultItem.length - 1);
            HashMap<String, Double> category = new HashMap<String, Double>();

            for (int i = 0; i < this.KNN; i++)
            {
                String docID = TraDocIDSet[assistArr[(assistArr.length - 1) - i]];
                String categoryID = this.categoryMap.get(docID);

                if (categoryID == null)
                {
                    System.err.println("The document " + docID
                            + ".xml is not in the category list");
                }

                if (category.containsKey(categoryID))
                {
                    double value = category.get(categoryID);
                    double similarity = resultItem[assistArr[(assistArr.length - 1)
                            - i]];
                    category.put(categoryID, value + similarity);
                }
                else
                {
                    double similarity = resultItem[assistArr[(assistArr.length - 1)
                            - i]];
                    category.put(categoryID, similarity);
                }
            }

            /* Find most frequent category in KNN files */
            String KNNCategory = "";
            double max = 0.0;
            Set<String> categorySet = category.keySet();
            for (Iterator<String> it = categorySet.iterator(); it.hasNext();)
            {
                String categoryID = it.next();
                double value = category.get(categoryID);
                if (value > max)
                {
                    max = value;
                    KNNCategory = categoryID;
                }
            }

            return new ClassifyResult(KNNCategory, category);
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
            return null;
        }
    }

    /**
     * 用于对similarity result结果进行排序的快速排序方法
     * 
     * @param arr
     *            包含similarity result值的原始数组
     * @param assistArr
     *            用于协助记录特征词index参与排序的辅助数组
     * @param start
     *            待排序数组的起始元素下标
     * @param end
     *            待排序数组的最后一个元素下标
     */
    private void sort(double[] arr, int[] assistArr, int start, int end)
    {
        if (start < end)
        {
            int mid = partition(arr, assistArr, start, end);
            if (this.KNN >= mid)
            {
                sort(arr, assistArr, start, mid - 1);
                sort(arr, assistArr, mid + 1, end);
            }
            else
            {
                sort(arr, assistArr, start, mid - 1);
            }
        }
    }

    /**
     * 用于协助快排的partition函数
     * 
     * @param arr
     *            包含similarity result值的原始数组
     * @param assistArr
     *            用于协助记录特征词index参与排序的辅助数组
     * @param start
     *            待排序数组的起始元素下标
     * @param end
     *            待排序数组的最后一个元素下标
     */
    private int partition(double[] arr, int[] assistArr, int start, int end)
    {
        // 找到end + start / 2位置对应于assistArr辅助数组的词的index，
        // 然后通过该index在原始result数组中找到该词的similarity值用于排序
        double pivot = arr[assistArr[(end + start) / 2]];

        // 交换中间元素和数组最后一个元素
        int tmpIndex = assistArr[end];
        assistArr[end] = assistArr[(end + start) / 2];
        assistArr[(end + start) / 2] = tmpIndex;

        int small = start - 1;
        for (int big = start; big < end; big++)
        {
            if (arr[assistArr[big]] <= pivot)
            {
                small++;
                int tmp = assistArr[big];
                assistArr[big] = assistArr[small];
                assistArr[small] = tmp;
            }
        }
        int tmp = assistArr[small + 1];
        assistArr[small + 1] = assistArr[end];
        assistArr[end] = tmp;

        return small + 1;
    }

    /**
     * 判断该docID所指定的xml分类是否正确，正确返回true，否则返回false
     * 
     * @param docID
     * @param category
     * @return
     */
    protected boolean evaluate(String docID, String category)
    {
        if (this.categoryMap.get(docID).equals(category))
            return true;
        else
            return false;
    }

    protected XMLDoc getXMLDoc(File xml)
    {
        try
        {
            String xmlName = xml.getName();
            int partition = xmlName.lastIndexOf(".");
            String docID = xmlName.substring(0, partition);

            if (!this.XMLDocMap.containsKey(docID))
            {
                Document doc = this.reader.read(xml);
                doc = this.contracter.contract(doc, NESTING | REPETITION
                        | ATTRIBUTE);
                XMLDoc XMLDoc = new XMLDoc(doc);
                XMLDoc.setDocID(docID);

                /* 保存备忘录 */
                this.XMLDocMap.put(docID, XMLDoc);

                return XMLDoc;
            }
            else
            {
                return this.XMLDocMap.get(docID);
            }
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
            return null;
        }
    }

    protected String[] exploreDocID(final ArrayList<File> fileArr)
    {
        String[] docIDSet = new String[fileArr.size()];
        int index = 0;
        for (File file : fileArr)
        {
            String fileName = file.getName();
            int partition = fileName.lastIndexOf(".");
            String docID = fileName.substring(0, partition);
            docIDSet[index++] = docID;
        }
        return docIDSet;
    }

    protected HashMap<String, String> exploreCategory()
    {
        /* Read category describe file */
        try
        {
            HashMap<String, String> categoryMap = new HashMap<String, String>();
            BufferedReader categoryFileReader = new BufferedReader(
                    new FileReader(Configuration.ResourcePath + "/categories.csv"));
            String line = "";

            /*
             * Hash each file id to each category according to categories.csv
             * information
             */
            StringTokenizer stk = null;
            String delim = "\t";
            delim += ",";
            while ((line = categoryFileReader.readLine()) != null)
            {
                /* Record each file category id and file id information */
                String categoryID = "";
                String fileID = "";

                stk = new StringTokenizer(line, delim);
                fileID = stk.nextToken();
                categoryID = stk.nextToken();

                // 由于这里训练使用的文件是2112299_10268.xml这样的形式的，所以在hash的时候也采用这种模式
                categoryMap.put(categoryID + "_" + fileID, categoryID);
            }
            return categoryMap;
        }
        catch (FileNotFoundException e)
        {
            e.printStackTrace();
            return null;
        }
        catch (IOException e)
        {
            e.printStackTrace();
            return null;
        }
    }

    protected File[] getFileList(final File directory)
    {
        ArrayList<File> fileArr = new ArrayList<File>();
        for (File f : directory.listFiles())
        {
            if (f.isFile())
            {
                fileArr.add(f);
            }
            else
            {
                for (File child : this.getFileList(f))
                {
                    fileArr.add(child);
                }
            }
        }
        int index = 0;
        File[] files = new File[fileArr.size()];
        for (File f : fileArr)
        {
            files[index++] = f;
        }
        return files;
    }
}
